地图显示概述

最后更新时间:2021年2月5日

功能介绍

功能描述

地图显示是GIS应用中最基础的功能,基本上所有的GIS功能都是以地图为基础,或以地图为媒介来展示功能的。MapGIS Mobile采用高性能地图渲染引擎,充分利用GPU的加速能力,结合各种调度机制和缓存策略,使得地图能够快速浏览展示。

应用场景

全面支持MapGIS地图显示(二维、三维),包括移动端离线地图、在线地图服务(含MapGIS地图、OGC服务)。这里介绍常见的MapGIS二维地图显示,包括离在线的地图文档显示、瓦片地图显示、矢量图层显示。 MapGIS地图显示功能在移动GIS中非常关键,尤其是离线地图的支持,提供离线地图与在线地图的同步机制。地图显示是GIS开发的基础,借助于MapGIS平台进行地图显示,为用户提供了方便快捷的移动地图显示方式。

支持对第三方地图服务的显示,比如天地图、百度地图、高德地图、OSM地图、Google地图、其他的地图服务,可提供给人们更多的选择。 第三方地图服务在移动GIS中扮演着举足轻重的作用。比如:在国内使用地图时,使用天地图、百度地图显示的地标、道路等信息显示的更加详细,当您在国外旅游时,使用Google地图等地图服务显示的信息会更加详细。

除此之外,支持自定义地图服务扩展,可在地图视图中加载自定义切片规则的地图数据,可对接各种矢量、瓦片地图数据或服务,具备良好的扩展性。

功能介绍
1
离线矢量地图显示

开发者可通过该功能,实现移动端加载离线矢量地图。移动端离线矢量地图,须通过移动转换工具将桌面端组织制作的地图文档转换为移动端离线数据包,并拷贝到移动设备才能使用。

2
离线瓦片地图显示

开发者可通过该功能,实现移动端加载离线瓦片地图。瓦片地图是指将一定范围内的地图按照一定的尺寸和格式,按缩放级别或者比例尺,切成若干行和列的正方形栅格图片,对切片后的正方形栅格图片被形象的称为瓦片(Tile)。离线瓦片地图加载,须将裁剪后的瓦片数据拷贝到移动端设备。

3
在线矢量地图显示

开发者可通过该功能,实现移动端加载在线矢量地图。移动端在线矢量地图加载,须提供可用的在线矢量地图服务,可通过MapGIS IGServer发布矢量地图服务或OGC服务。

4
在线瓦片地图显示

开发者可通过该功能,实现移动端加载在线瓦片地图。瓦片地图是指将一定范围内的地图按照一定的尺寸和格式,按缩放级别或者比例尺,切成若干行和列的正方形栅格图片,对切片后的正方形栅格图片被形象的称为瓦片(Tile)。移动端在线瓦片地图加载,须提供可用的在线瓦片地图服务,可通过MapGIS IGServer发布瓦片地图服务或OGC服务。

5
在线WMS显示

开发者可通过该功能,实现移动端加载OGC服务的WMS地图,须提供可用的WMS服务,可通过MapGIS IGServer发布WMS或者调用其他第三方的WMS服务。

6
在线WMTS显示

开发者可通过该功能,实现移动端加载OGC服务的WMTS地图,须提供可用的WMTS服务,可通过MapGIS IGServer发布WMTS或者调用其他第三方的WMTS服务。

7
第三方地图显示

开发者可通过该功能,实现移动端加载第三方地图,如天地图、百度地图、高德地图、OSM地图、Google地图等,须确保网络能正常使用且第三方地图服务可用。

8
自定义地图服务显示

开发者可通过该功能,实现移动端加载自定义的地图,即自定义切片规则的地图数据。

9
3D灰度模型

开发者可通过该功能,实现移动端加载3D灰度模型,其矢量数据必须通过桌面工具进行3D灰度模型的预处理设置。

基本原理

移动端地图显示与MapGIS桌面软件地图显示设计一脉相承,均通过地图文档—地图—图层来组织地图,支持离线模式与在线模式的地图加载。其显示的基本原理也与桌面端、Web端类似,采用一个地图视图控件作为地图容器装载地图,调用地图容器(com.zondy.mapgis.map.view.mapview.MapView)的相关接口即可加载移动二维地图。

地图显示逻辑结构.png

功能接口

MapGIS Mobile for Android SDK提供了多个接口来实现地图加载功能,都由com.zondy.mapgis.map.view.mapview程序包中的MapView类提供,例如loadFromFile/loadFromFileAsync、loadFromDocument/loadFromDocumentAsync、setMap/setMapAsync等方法。核心接口如下:

方法 接口 适用类型
加载地图文档 loadFromFile / loadFromFileAsync 离线、在线地图
加载地图文档对应索引的地图 loadFromDocument / loadFromDocumentAsync 离线、在线地图
基于ServerLayer实例化Map setMap / setMapAsync 离线瓦片、在线地图

可以观察到,每一类方法都包括同步和异步的接口,它们具有不同的应用场景:

同步方法:同步方法一般用于加载小数据量的离线地图,在主线程中加载地图数据,对于数据量大的离线数据则可用异步方法;

异步方法:异步方法一般用于加载数据量大的离线地图或者在线地图,通过子线程加载地图数据,提高显示效率。例如当离线地图图层数目较多,或者加载在线服务图层的时候,地图加载较慢,此时不能把地图加载放到主线程,而需要在子线程加载地图。

说明:除直接调用API接口中的异步方法,可以单独开一个子线程使用同步方法加载地图。

基本流程

1

数据准备

如果要使用离线地图:需先在MapGIS桌面工具中组织地图文档,然后再将地图文档与数据库文件一起拷贝到移动设备中。

如果要使用在线地图:首先要发布自己的服务并获取服务地址,或者获取已有的服务地址,然后直接在移动端调用,或组织为地图文档使用。

具体的操作可参考数据组织配置->地图数据组织模块。

2

创建工程


以Android Studio软件作为开发工具,打开软件后新建项目。

新建项目.png

3

引入二次开发库


从云生态圈的开发世界中获取MapGIS Mobile for Android二次开发包,其中libs文件夹中的文件就是实现MapGIS移动GIS功能需要的具体开发库,包括jar包和so库两部分,使用时需将其引入到项目中。

(1) 导入jar包:切换工程目录结构为“Project”模式,将jar包拷贝到工程的“app\libs”目录下。

拷贝开发包.png

添加完成之后,选择加入的jar包,右键单击,在弹出的菜单中选择“Add As Library”,弹出“Create Library”对话框,选择app模块,点击“OK”即可。等待项目同步更新完成,查看当前module中的“build.gradle”脚本文件,发现其中在dependencies标签中增加了jar的依赖,到此完成jar的导入。

build.gradle脚本文件.png

(2) 导入so库:首先同样将SDK中libs目录下的armeabi、armeabi-v7a、arm64-v8a文件夹拷贝到模块的libs目录下,如果已有这些目录,则直接将*.so库文件复制到对应的目录中。(配置中可指定适配的架构)

添加so库文件.png

然后打开对应module下的build.gradle脚本文件,在android标签中,添加如下代码,完成后sync同步工程即可。

sourceSets {
    main {
        jniLibs.srcDirs = ['libs'];
    }
}

0105AS配置build.gradle文件.png

4

设置移动端设备资源使用权限

一般开发APP时,由于APP通常会使用存储卡、网络、相机、定位等资源,因此需要为应用添加移动终端设备资源的使用权限,在AndroidManifest.xml(类似于系统配置文件)中编写对应权限标签。

0419设置AS应用程序权限.png

根据应用的实际需求添加对应的应用程序权限。例如,此程序用于实现地图显示功能,需要进行MapGIS环境初始化、授权操作,加载离线地图,所以需要应用具备读/写手机存储的权限、联网权限、读取手机状态的权限。

<!-- 允许应用程序向外部存储写入数据,如SD卡上写文件 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 允许应用程序读取外部存储数据 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 允许应用程序只读访问手机状态 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!-- 允许应用程序访问网络,可能产生GPRS流量 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 允许应用程序获取WiFi网络的信息,WiFi信息会用于网络定位功能中 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- 允许应用程序访问有关网络的信息,用于支持提供运营商信息相关的接口 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 允许应用程序改变WiFi状态 -->
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<!-- 允许应用程序访问精确位置,通过GPS进行定位 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- 允许应用程序访问近似位置,通过网络进行定位 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- 允许应用程序震动,用于草图编辑器功能 -->
<uses-permission android:name="android.permission.VIBRATE" />
<!-- 允许应用程序开启悬浮框 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

特别提示:需注意的是,在应用程序的targetSdkVersion版本大于等于23,手机的Android系统版本大于等于6.0时,对于读写手机存储、读取手机状态等危险权限,需要在Java代码中通过代码来动态申请权限。

针对V10.5.0.10版本,建议将targetSdkVersion设置为28及以下版本,暂不支持targetSdkVersion 29及以上版本(将在后续更新的版本中支持)。若手机的Android系统版本大于等于10时,暂时也将targetSdkVersion设置为28及以下版本。

动态申请手机权限一般放在APP启用页面或首页实现。例如,新建一个启动页面(WelcomeActivity),编写相关代码参考如下:


    //应用程序权限
    private static final int MY_PERMISSIONS_REQUEST_CODE = 1;
    private String[] permissionArr=new String[]{
            Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.READ_PHONE_STATE
    };


    /**
     * 检查权限,没有授权即请求授权
     * @param permissionArray
     */
    public void checkPermissions(String[] permissionArray){
        //检查所有的权限,如果所有权限具备,则初始化MapGIS开发环境
        if (hasGetAllPermission(permissionArr)) {
            //初始化MapGIS开发环境
            initMapGISEnvironment();
        }
        //不是所有权限都具备,则继续检查请求
        else {
            for (String permission:permissionArray){
                if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED){
                    //没有授权
                    requestPermissions(permission);
                }
            }
        }
    }

    /**
     * 检验是否所有需要的权限都已申请,只有当所有权限都授予之后,才能进行MapGIS的环境初始化
     */
    public boolean hasGetAllPermission(String[] permissionArray){
        List needPermission=new ArrayList();
        for(String permission:permissionArr){
            if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
                needPermission.add(permission);
            }
        }
        //如果所有权限都已具备,则返回true,否则返回false
        if (needPermission.size()==0) {
            return true;
        }
        else return false;
    }

    /**
     * 请求权限
     * @param permission
     */
    public void requestPermissions(String permission){
        ActivityCompat.requestPermissions(this,new String[]{permission},MY_PERMISSIONS_REQUEST_CODE);
    }

    /**
     * 请求权限回调
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        if (requestCode == MY_PERMISSIONS_REQUEST_CODE){
            if (grantResults.length > 0){
                //权限没有授予
                if (grantResults[0] != PackageManager.PERMISSION_GRANTED){
                    switch (permissions[0]){
                        case Manifest.permission.WRITE_EXTERNAL_STORAGE:
                            showMyDialog("此程序需要存储的读写权限,请点击设置前往权限模块授予。\n未授予权限程序无法正常工作");
                            break;
                        case Manifest.permission.READ_PHONE_STATE:
                            showMyDialog("此程序需要电话权限,请点击设置前往权限模块授予。\n未授予权限程序无法正常工作");
                            break;
                        default:
                            break;
                    }
                }
                //权限已授予
                if (grantResults[0] == PackageManager.PERMISSION_GRANTED){
                    //如果权限授予了,则重新判断所有权限,没有授予的继续请求
                    checkPermissions(permissionArr);
                }
            }
        }
    }

    /**
     * 弹出自定义对话框:提示权限的重要性,并引导用户前往程序的应用管理界面手动开启权限
     * @param message
     */
    private void showMyDialog(String message){
        AlertDialog.Builder builder=new AlertDialog.Builder(this);
        builder.setTitle("权限申请");
        builder.setMessage(message);
        builder.setPositiveButton("设置", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                //打开系统中应用设置的界面
                Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                intent.setData(Uri.parse("package:" + getPackageName()));
                startActivity(intent);

                finish();
            }
        });
        builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                //如果取消,则退出应用
                finish();
            }
        });
        builder.setCancelable(false);//点击返回键和空白区域不取消对话框
        builder.show();
    }

代码说明:动态申请手机权限,弹框请求用户授权,当具备所须的所有权限后再调用initMapGISEnvironment()方法(即下一步骤实现的方法)进行MapGIS环境初始化与授权请求验证,通过之后再进入主页面(MainActivity)。

5

环境初始化与授权请求

要使用MapGIS移动平台提供的功能,导入开发库后,还需要初始化MapGIS环境、请求移动端授权,这样GIS功能才能得以使用。移动开发授权功能对应的API程序包为com.zondy.mapgis.environment,使用环境类(Environment)的接口方法实现。

必须在获取手机相关权限后进行MapGIS环境初始化与移动端授权请求,然后才能使用MapGIS移动端相关功能接口。例如,在APP启动页(WelcomeActivity)进行MapGIS环境初始化与授权,实现initMapGISEnvironment()。具体操作为:展开工程中的src/main/java目录,打开“WelcomeActivity.java”文件,在initMapGISEnvironment()中调用Environment类的initialize()方法进行环境初始化,然后调用requestAuthorization()方法请求授权。代码如下:

    import com.zondy.mapgis.environment.Environment;

     /**
     * 初始化MapGIS开发环境
     */
    public void initMapGISEnvironment() {

        /*
         * 构建环境目录路径:
         * 此程序环境目录路径为/storage/emulated/0/MapGISSample/
         * 所以数据需要拷贝到手机存储的根目录下
         */
        String strRootPath = android.os.Environment.getExternalStorageDirectory().getPath() + "/MapGISSample/";

        /*
         * 环境初始化:
         * 要求:必须执行,必须在调用MapGIS Mobile for Android SDK各组件之前调用
         * 执行工作:会自动建立根目录结构,程序所需要的文件、缓存等数据会存放到此目录中
         */
        com.zondy.mapgis.environment.Environment.initialize(strRootPath, this);

        /*
         * 请求授权:
         * 要求:必须执行,MapGIS移动开发平台采用授权机制
         * 只有通过授权之后,才能使用MapGIS相关功能及控件
         */

        com.zondy.mapgis.environment.Environment.requestAuthorization(this, new Environment.AuthorizeCallback() {
            @Override
            public void onComplete() {
                System.out.println("授权完成");

                //当授权完成时,跳转到主界面
                startActivity(new Intent(WelcomeActivity.this,MainActivity.class));

            }
     });

代码说明:调用initialize()进行MapGIS环境初始化,会自动建立MapGIS系统环境目录结构;然后再调用requestAuthorization()请求MapGIS移动授权验证,当程序执行到requestAuthorization方法的回调中即表明已经成功授权,即可跳转到主页面,在主页面使用MapGIS Mobile for Android SDK的各组件功能。

特别提示:必须先进行手机授权,具备手机授权后进行MapGIS环境初始化与移动端授权请求,移动端授权验证通过后才能使用MapGIS移动端相关GIS功能接口

6

新增地图控件并编码实现地图显示

在APP主界面中新增地图控件并编码实现地图显示功能。例如,在APP主界面的布局文件(activity_main.xml)中加载地图控件,然后在主页面的Java文件(MainActivity.java)中实现离线地图加载功能。

(1)页面布局(例如res/layout/activity_main.xml)——加载地图控件 在工程中展开res/layout目录,打开activitymain.xml文件,初始化加载一个MapView视图(com.zondy.mapgis.map.view.mapview.MapView)并设置其ID,即在activitymain.xml文件中新增加载MapView视图的代码。

0420AS添加MapView控件.png

<com.zondy.mapgis.map.view.mapview.MapView
    android:id="@+id/mapview_id"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
</com.zondy.mapgis.map.view.mapview.MapView>

代码解析:地图控件(com.zondy.mapgis.map.view.mapview.MapView),作为一个地图容器,用于装载显示地图,可通过其ID获得此地图容器对象。

(2)代码编写(例如src下的MainActivity.java文件)——地图控件中加载地图数据

在工程中展开src的目录,打开MainActivity.java文件,先获取移动设备SD卡上放置离线地图数据文件的路径,然后通过地图视图对象调用loadFromFile/loadFromFileAsync接口加载离线地图。

0421AS加载地图代码.png

方式一:同步加载离线地图

    import com.zondy.mapgis.map.view.mapview.MapView;

    //定义地图视图对象    
    private MapView mapView = null;
    //离线地图文档文件路径
    private String offlineMapPath = android.os.Environment.getExternalStorageDirectory().getPath() + "/MapGISSample/Map/MapShow/WuHan/WuHan.mapx";

    //获取地图容器
    mapView = (MapView) findViewById(R.id.mapview_id);
   
    //加载地图文档-同步方法(离线地图文档路径)
    mapView.loadFromFile(offlineMapPath);

代码解析:在MainActivity.java的onCreate方法中编写同步加载地图的具体实现代码,主要接口为loadFromFile,此为同步加载地图的统一接口,参数为地图文件的路径。

方式二:异步加载离线地图

    import com.zondy.mapgis.map.view.mapview.MapView;
    import com.zondy.mapgis.map.Map;
    import com.zondy.mapgis.geometry.Rect;

    //定义地图视图对象    
    private MapView mapView = null;
    //定义地图对象
    private Map map = null;

    //离线地图文档文件路径
    private String offlineMapPath = android.os.Environment.getExternalStorageDirectory().getPath() + "/MapGISSample/Map/MapShow/WuHan/WuHan.mapx";

    //获取地图容器
    mapView = (MapView) findViewById(R.id.mapview_id);
   
    //加载地图文档-异步方法(离线地图文档路径)
    mapView.loadFromFileAsync(offlineMapPath);

    //异步加载地图回调监听
    mapView.setMapLoadListener(new MapView.MapViewMapLoadListener() {
        //开始加载事件
        @Override
        public void mapViewWillStartLoadingMap(MapView arg0, String arg1) {
            Toast.makeText(MainActivity.this, "开始加载地图", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void mapViewDidFinishLoadingMap(MapView arg0, String arg1) {
            Toast.makeText(MainActivity.this, "地图加载完成", Toast.LENGTH_SHORT).show();
            //缩放地图到指定范围
            //Rect rect = new Rect(12705276.572663, 3542912.332349, 12746062.170780, 3607262.942711);
            Map map = mapView.getMap();
            Rect rect = null;
            if(map != null)
                rect = map.getViewRange();
            mapView.zoomToRange(rect, false);
        }

        @Override
        public void mapViewDidFailLoadingMap(MapView arg0, String arg1) {
            Toast.makeText(MainActivity.this, "地图加载失败", Toast.LENGTH_SHORT).show();
        }
    });

代码解析:在MainActivity.java的onCreate方法中编写异步加载地图的具体实现代码,主要接口为loadFromFileAsync,此为异步加载地图的统一接口,参数为地图文件的路径。其中,异步加载地图通过调用setMapLoadListener()进行地图加载回调监听,可在完成地图加载时设置地图初始显示范围。

7

调试运行APP


单击菜单栏上的Run|Run ‘app’,或者工具栏上的 图标,在弹出的对话框中选择目标设备(模拟器或者真机),单击“OK”按钮,即可将项目安装到移动设备上。

运行设备选择.png

将项目安装到模拟器或者手机上,并运行。若初次运行基于MapGIS Mobile 10.3开发的app,将在app中弹出对话框进行授权认证,许可认证后的移动端设备再次运行使用此app则不会再弹框认证。

在外部模拟器(Droid4X)运行app,确保地图数据已经拷贝到模拟器的SD卡目录下,初次运行时弹出对话框进行授权认证,许可认证成功后进入地图显示界面。

08外部模拟器运行app(授权认证).jpg

09外部模拟器运行app(地图显示).jpg

在手机上运行app,确保地图数据已经拷贝到手机的SD卡目录下,初次运行时弹出对话框进行授权认证,许可认证成功后进入地图显示界面。

10手机上运行app的地图显示效果.jpg